package com.googlecode.connectlet.machine.provider;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.spec.AlgorithmParameterSpec;

import com.googlecode.connectlet.machine.MachinePacket;
import com.googlecode.connectlet.packet.PacketException;
import com.googlecode.connectlet.packet.PacketHandler;
import com.googlecode.connectlet.packet.PacketReader;
import com.googlecode.connectlet.util.Pool;

class MachinePool extends Pool<Socket> {
	private static final byte[] MACHINE_PING_DATA = new
			MachinePacket(MachinePacket.TAG_PING).getEncoded();

	private InetAddress ip;
	private int port;

	public MachinePool(InetAddress ip, int port) {
		this.ip = ip;
		this.port = port;
	}

	@Override
	protected Socket makeObject() {
		try {
			return new Socket(ip, port);
		} catch (Exception e) {
			return null;
		}
	}

	@Override
	protected boolean activateObject(Socket obj) {
		byte[] b = new byte[MachinePacket.HEAD_SIZE];
		try {
			obj.getOutputStream().write(MACHINE_PING_DATA);
			InputStream in = obj.getInputStream();
			int bytesRead = 0;
			while (true) {
				int n = in.read(b, bytesRead, b.length - bytesRead);
				if (n < 0) {
					return false;
				}
				bytesRead += n;
				if (bytesRead == b.length) {
					return true;
				}
			}
		} catch (IOException e) {
			return false;
		}
	}

	@Override
	protected void destroyObject(Socket obj) {
		try {
			obj.close();
		} catch (IOException e) {/**/}
	}
}

public class MachineConnection implements AlgorithmParameterSpec {
	private InetAddress ip;
	private int port;
	private MachinePool pool;

	protected MachineConnection(MachineConnection conn) {
		ip = conn.ip;
		port = conn.port;
		// Share one pool with the original instance
		pool = conn.pool;
	}

	public MachineConnection(InetAddress ip, int port) {
		pool = new MachinePool(ip, port);
	}

	public MachineConnection(String host, int port) throws IOException {
		this(InetAddress.getByName(host), port);
	}

	public InetAddress getIp() {
		return ip;
	}

	public int getPort() {
		return port;
	}

	private byte[] buffer = new byte[32768];

	public MachinePacket process(MachinePacket request) throws PacketException {
		Socket socket = pool.borrowObject();
		if (socket == null) {
			throw new PacketException("Connection refused: " + this);
		}
		final MachinePacket[] packet = new MachinePacket[] {null};
		PacketReader reader = new PacketReader(MachinePacket.getParser(),
				new PacketHandler() {

			@Override
			public void onData(byte[] b, int off, int len) {
				packet[0] = new MachinePacket(b, off);
			}
		});
		try {
			socket.getOutputStream().write(request.getEncoded());
			InputStream in = socket.getInputStream();
			while (packet[0] == null) {
				int len = in.read(buffer);
				reader.onData(buffer, 0, len);
			}
			return packet[0];
		} catch (IOException e) {
			throw new PacketException(e.getMessage());
		} finally {
			pool.returnObject(socket);
		}
	}

	public MachineKey generateMachineKey() {
		return generateMachineKey(null);
	}

	public MachineKey generateMachineKey(byte[] key) {
		try {
			MachinePacket request = (key == null ?
					new MachinePacket(MachinePacket.TAG_KEY_NEW) :
					new MachinePacket(MachinePacket.TAG_KEY_SET, 0, 0, key));
			return new MachineKey(this, process(request).getKeyId());
		} catch (Exception e) {
			throw new RuntimeException("Connection refused: " + this, e);
		}
	}

	@Override
	public String toString() {
		return ip + "/" + port;
	}
}